Skip to content

Harden Python preview collection, lifetime, interface, and async safety - #198

Open
leileizhang (lei9444) wants to merge 15 commits into
mainfrom
lei9444-fix-python-preview-blockers
Open

leileizhang (lei9444) wants to merge 15 commits into
mainfrom
lei9444-fix-python-preview-blockers

Conversation

@lei9444

@lei9444 leileizhang (lei9444) commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A generated Python WinRT object can outlive RoApartment and release its COM reference after RoUninitialize, crashing the process (#189). Three other preview-safety gaps permit partial JSON mutations, unchecked interface pointers, or an invalid low-level Async receiver (#196).

Changes

  • Release dynwinrt-owned raw values, generated wrappers, and COM-bearing arrays/structs (including nested and cloned values) on the owner thread before the final dynwinrt-managed apartment close, even without projected_lifetime_scope(). Balance nested/manual initialization and explicit scopes; do not consume externally owned references. Objects retained past close report released and reject further use. Unsafe pending async work blocks close for retry rather than being cancelled implicitly.
  • Reject native None before mutating stock JsonArray/JsonObject, including bulk writes and interface views, and align .pyi with runtime/--no-pyi annotations. Use JsonValue.create_null_value() for JSON null; custom generic collections retain their valid native-null behavior.
  • Validate generated interface constructors by IID before storing a pointer; reject Async receivers before call_0()/call_1() native dispatch.
  • Add shutdown_python_callbacks() for embedding hosts. After the host stops new calls and settles in-flight callbacks, it closes Python-backed callback entrypoints before Py_FinalizeEx; retained native aliases then return a closed error without attaching Python. Apartment cleanup does not revoke external aliases or cancel their work.

Verification and boundaries

The original unscoped #189 Uri deletion and interpreter-exit reproductions now exit normally on x64 and ARM64. Installed-wheel Python tests, generated WinRT/implementation E2E, strict typing/stubtest, and exact-head Build, mixed-language coverage, and Python-release checks passed. Optional live WinUI fixtures and PR-triggered ARM64 release-wheel jobs are not covered by those hosted checks.

This PR does not make reentrant RoApartment.close() inside a synchronous native callback safe; that pre-existing, separate issue is addressed by #205. A host that skips the explicit callback shutdown protocol has only best-effort protection against external callbacks during the early interpreter-finalization race; no universal guarantee is claimed. If #205 merges first, reconcile apartment-depth and pending-close handling and revalidate the combined result before landing this PR. #189 remains open pending human review and merge; green CI alone is not approval. No release workflow, XML DOM nullability, or published preview.22 notes are changed.

Validate the receiver's stock runtime-class contract before JSON value writes, preflight bulk mutations, and keep custom generic collections nullable. Narrow verified stock class stubs and test real native behavior and strict consumers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Register owned native results at Python return boundaries so a closing scope releases raw values before apartment teardown, while preserving borrowed sources, scalar results, callback thread affinity, and released-value errors. Reproduce and guard the shutdown crash in subprocesses.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
QueryInterface-check standalone constructors before retaining or caching a pointer, preserve borrowed constructor sources on cache hits, and fail closed for unknown IIDs. Keep generic and observable projections safe with real WinRT and strict typing regressions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Require an Object before call_0/call_1 can prepare or dispatch a caller-specified native signature. Exercise pre-dispatch rejection and valid IID-cast low-level calls in an isolated Python process.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Observe raw outputs weakly so temporary casts and callback inputs drop without accumulating native references; keep projected wrappers strongly tracked and release surviving raw owners before apartment shutdown. Refresh seven generated Python snapshots for the interface-IID constructor change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Mixed-language test coverage

Workflow status: ✅ Passed

Layer Lines Functions Branches/regions
Rust, including native .pyd/.node 86.99% 82.58% 86.7% regions
Python aggregate 72.34% n/a 39.42% branches
Python runtime 98.17% n/a 94.57% branches
Generated Python WinRT projections 71.08% n/a 33.5% branches
Generated Python WinRT implementations 71.97% n/a 46.3% branches
JavaScript aggregate 21.91% 25.18% 57.61% branches
JavaScript runtime 44.27% 45.76% 78.99% branches
Generated WinRT projections 22.8% 18.7% 54.84% branches
Generated WinRT implementations 45.99% 59.19% 60.97% branches
Generated Classic COM projections 11.88% 23.97% 53.4% branches

View workflow run and download full HTML/LCOV/XML reports

leileizhang (lei9444) and others added 10 commits September 29, 2026 20:04
Match ProjectedLifetimeScope.track_native in the packaged Python stub and test the installed wheel surface, fixing hosted mypy.stubtest parity without changing native lifetime behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Match DynWinRTValue's owned-reference drop in the projection-only test double and accept the checked-constructor cache argument. Assert that an unconsumed temporary returns the native owner's reference count to baseline.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Make stock JsonArray/JsonObject runtime method inputs non-null in both normal and --no-pyi output, keeping generic IVector/IMap<IJsonValue> nullable. Verify generated source and inspect.signature contracts without changing JS/TS projections.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Weakly track independent DynWinRTArray and DynWinRTStruct owners at creation, extraction, and nested-field boundaries. Deterministically release surviving COM references at scope exit, reject all post-release access, and leave scalar containers live. Cover five previously crashing subprocess paths, nested arrays, and native reference balance.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise get_struct and nested Object-field clones through apartment teardown and owner reference balance, prove releasing a container preserves its external source identity, and verify borrowed native callback arrays survive an inner scope without adopting the parameter.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reject mismatched scalar/object and unsupported nested array elements before an ArrayData takes independent ownership. Reuse native WinRT argument validation and typed QueryInterface while preserving null, enum, Char16 and HRESULT aliases. Detect COM owners recursively in actual array payloads as well as declared metadata at every Python tracking boundary; reproduce and prevent both apartment-exit crashes with source-reference balance regressions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use the owned local IStringable fixture for deterministic parallel core tests instead of repeatedly activating cached stock Uri factories across test apartments. Retain stock Uri typed-QI and mismatched-array coverage in an isolated integration executable and Python subprocess, each repeating both paths inside one balanced apartment.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Weakly track raw, container, callback, implementation and async owners so the final managed RoUninitialize releases only their references on the owner thread. Guard cross-thread access and add an explicit callback shutdown gate for embedding hosts, with isolated real-finalization and generated-code regressions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Initialize the real binding module in both isolated Rust finalization children. Coverage can now exercise native callback owner shutdown without an installed Python wheel, using the same package bootstrap as the embedded-host gate test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reclassify the actual object and nested-struct fields after native setters return, including errors, without masking the original failure. Cover failed agile writes, partial writes, owner-thread balance and successful non-agile transitions on both architectures; document the separate pre-existing reentrant apartment-close limitation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant